跳到主要内容

CSS 中的 Flexbox

Flexbox 是什么

跟浮动布局相比,Flexbox 的可预测性更好,还能提供更精细的控制。它也能轻松解决困扰我们许久的垂直居中和等高列问题。

要说 Flexbox 有缺点的话,那一定是它那数不清的选项了。它给 CSS 引入了 12 个新属性,包括一些缩写属性。

给元素添加 display: flex,该元素变成了一个弹性容器(flex container),它的直接子元素变成了弹性子元素(flex item)。弹性子元素默认是在同一行按照从左到右的顺序并排排列。弹性容器像块元素一样填满可用宽度,但是弹性子元素不一定填满其弹性容器的宽度。弹性子元素高度相等,该高度由它们的内容决定。

之前提到的 display 值,比如 inline、inline-block 等,只会影响到应用了该样式的元素,而 Flexbox 则不一样。一个弹性容器能控制内部元素的布局。

子元素按照主轴线排列,主轴的方向为主起点(左)到主终点(右)。垂直于主轴的是副轴。方向从副起点(上)到副终点(下)。

例子构建一个网页

为了更好的学习 flex 布局,这里使用 flex 布局来构建一个网页

具体参考作者的 仓库代码

最终效果

网页的骨架

<body>
<div class="container">
<header>
<h1>Ink</h1>
</header>
<nav>
<!-- 导航菜单 -->
<ul class="site-nav">
<li><a href="/">Home</a></li>
<li><a href="/features">Features</a></li>
<li><a href="/pricing">Pricing</a></li>
<li><a href="/support">Support</a></li>
<li class="nav-right">
<a href="/about">About</a>
</li>
</ul>
</nav>

<!-- 大的主体板块 -->
<main class="flex">
<div class="column-main tile">
<h1>Team collaboration done right</h1>
<p>Thousands of teams from all over the
world turn to <b>Ink</b> to communicate
and get things done.</p>
</div>

<!-- 侧边栏包含两个上下叠放的小板块 -->
<div class="column-sidebar">
<div class="tile">
<form class="login-form">
<h3>Login</h3>
<p>
<label for="username">Username</label>
<input id="username" type="text"
name="username"/>
</p>
<p>
<label for="password">Password</label>
<input id="password" type="password"
name="password"/>
</p>
<button type="submit">Login</button>
</form>
</div>
<div class="tile centered">
<small>Starting at</small>
<div class="cost">
<span class="cost-currency">$</span>
<span class="cost-dollars">20</span>
<span class="cost-cents">.00</span>
</div>
<a class="cta-button" href="/pricing">
Sign up
</a>
</div>
</div>
</main>
</div>
</body>

然后给网页加上一些基础的样式

:root {
box-sizing: border-box;
}

*,
::before,
::after {
box-sizing: inherit;
}

body {
background-color: #709b90;
font-family: Helvetica, Arial, sans-serif;
}

body * + * {
margin-top: 1.5em;
}

.container {
max-width: 1080px;
margin: 0 auto;
}

基础样式的效果

创建一个基础的菜单

要实现以下效果

要实现这个菜单,需要考虑让哪个元素做弹性容器,这个元素的子元素便会成为弹性子元素。在本例中,弹性容器应该是无序列表<ul>。它的子元素,即列表项 <li> 就是弹性子元素。

代码如下所示

<ul class="site-nav">
<li><a href="/">Home</a></li>
<li><a href="/features">Features</a></li>
<li><a href="/pricing">Pricing</a></li>
<li><a href="/support">Support</a></li>
<li class="nav-right">
<a href="/about">About</a>
</li>
</ul>

接下来分几步构建这个菜单。首先,给列表加上 dislay: flex。然后覆盖浏览器默认的列表样式和猫头鹰选择器设置的顶部外边距。同时添加颜色。

加上后它的效果:

提示

猫头鹰选择器,因为 + 这种方式酷似猫头鹰,被称为猫头鹰选择器。

* + * {   margin-top: 5px; }

上面定义了一个简单的猫头鹰选择器

+: 相邻兄弟选择器(Adjacent sibling selector)可选择紧接在另一元素后的元素,且二者有相同父元素。

*: 通配符,代表任意元素。

使用例:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
<script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.js"></script>
<style>
.a {background: pink;}
.warp div + div {margin-top: 20px;}
</style>
</head>
<body>
<div class="warp">
<div class="a">div</div>
<div class="a">div</div>
<div class="a">div</div>
<div class="a">div</div>
</div>
</body>
</html>

添加内边距和间隔

现在菜单看起来比较“单薄”,加上内边距能让它变“饱满”一些。

如果你还不太熟悉构建这样的菜单(不管用 Flexbox 还是其他布局方式),那么请注意一下如何实现。在这个例子里,应当把菜单项内边距加到内部的 <a> 元素上,而不是 <li> 元素上。因为整个点击区域的外观和行为应当都符合用户对一个菜单链接的预期,而链接的行为来自于 <a> 元素,所以如果把 <li> 做成一个好看的大按钮,里面只有很小的区域 <a> 可以点击,就不符合用户预期

给容器和菜单链接都加上内边距后

注意这里的链接被设置为块级元素。如果链接还是行内元素,那么它给父元素贡献的高度会根据行高计算,而不是根据内边距和内容,这样不符合预期。另外,这里给水平方向设置的内边距比垂直方向的要多一点,因为从美学上来讲这样更让人愉悦

接下来,给菜单项添加间隔。常规的外边距就能做到这一点。更棒的是,Flexbox 允许使用 margin: auto 来填充弹性子元素之间的可用空间。Flexbox 还允许将最后的菜单项移动到右侧。加上外边距后,菜单就完成了(如图 5-6 所示)

设置的效果

代码清单 5-5 只给一个元素(About)加了 auto 外边距。如果希望将 Support 菜单项和 About 菜单项都推到右侧,则可以把 auto 外边距加到 Support 菜单项上。

弹性子元素的大小

前面的代码使用外边距给弹性子元素设置了间距。你可以用 width 和 height 属性设置它们大小,但是比起 margin、width、height 这些常见属性,Flexbox 提供了更多更强大的选项。

我们来看一个更有用的 Flexbox 属性:flex

flex 属性控制弹性子元素在主轴方向上的大小(在这里指的元素的宽度)。

代码清单 5-6 将给网页的主区域应用弹性布局,并使用 flex 属性控制每一列的大小。产生的主区域效果如图 5-7 所示

现在内容已经分为了两列:左侧较大的区域是网页的主要内容,右侧是登录表单和一个小的价格盒子。因为目前还没有特别设置两列的宽度,所以是根据内容自适应的宽度。在我的屏幕上 (如图 5-7 所示),它们没有完全填满可用空间,尽管在更小的窗口下可能不会出现这种状况。

使用 flex 属性设置列宽,使用 flex 属性给两列分别赋以 2/3 和 1/3 的宽度

.column-main {
flex: 2;
}

.column-sidebar {
flex: 1;
}

现在两列扩展宽度填满空间,它们的宽度加起来等于 nav 导航条的宽度,同时左侧主区域的宽度是侧边栏宽度的两倍。Flexbox 已贴心地完成了所有数学计算。来看一下它的具体计算过程。

flex 属性是三个不同大小属性的简写:flex-grow、flex-shrink 和 flex-basis。在代码清单 5-7 里,只提供了 flex-grow 的值,剩下的两个属性是默认值(分别是 1 和 0%),因此 flex: 2 等价于 flex: 2 1 0%。通常首选简写属性,但也可以分别声明三个属性

.box {
flex-grow: 2;
flex-shrink: 1;
flex-basis: 0%;
}

flex-basis 定义了元素大小的基准值,即一个初始的“主尺寸”。flex-basis 属性可以设置为任意的 width 值,包括 px、em、百分比。它的初始值是 auto,此时浏览器会检查元素是否设置了 width 属性值。如果有,则使用 width 的值作为 flex-basis 的值;如果没有,则用元素内容自身的大小。如果 flex-basis 的值不是 auto,width 属性会被忽略。

每个弹性子元素的初始主尺寸确定后,它们可能需要在主轴方向扩大或者缩小来适应(或者填充)弹性容器的大小。这时候就需要 flex-grow 和 flex-shrink 来决定缩放的规则。

每个弹性子元素的 flex-basis 值计算出来后,它们(加上子元素之间的外边距)加起来会占据一定的宽度。加起来的宽度不一定正好填满弹性容器的宽度,可能会有留白(如图上图所示)。

多出来的留白(或剩余宽度)会按照 flex-grow(增长因子)的值分配给每个弹性子元素,flex-grow 的值为非负整数。如果一个弹性子元素的 flex-grow 值为 0,那么它的宽度不会超过 flex-basis 的值;如果某个弹性子元素的增长因子非 0,那么这些元素会增长到所有的剩余空间被分配完,也就意味着弹性子元素会填满容器的宽度(如图 5-9 所示)

flex-grow 的值越大,元素的“权重”越高,也就会占据更大的剩余宽度。一个 flex-grow: 2 的子元素增长的宽度为 flex-grow: 1 的子元素的两倍(如图 5-10 所示)

还记得前面的三个板块吗?简写声明 flex: 2flex: 1 设置了一个弹性基准值为 0%,因此容器宽度的 100% 都是剩余宽度(减去两列之间 1.5em 的外边距)。剩余宽度会分配给两列:第一列得到 2/3 的宽度,第二列得到 1/3 的宽度(如图 5-11 所示)

提示

推荐使用简写属性 flex,而不是分别声明 flex-grow、flex-shrink、flex-basis。与大部分简写属性不一样,如果在 flex 中忽略某个子属性,那么子属性的值并不会被置为初始值。相反,如果某个子属性被省略,那么 flex 简写属性会给出有用的默认值:flex-grow 为 1、flex-shrink 为 1、flex-basis 为 0%。这些默认值正是大多数情况下所需要的值。

flex-shrink 属性与 flex-grow 遵循相似的原则。计算出弹性子元素的初始主尺寸后,它们的累加值可能会超出弹性容器的可用宽度。如果不用 flex-shrink,就会导致溢出(如图 5-12所示)

每个子元素的 flex-shrink 值代表了它是否应该收缩以防止溢出。如果某个子元素为 flex-shrink: 0,则不会收缩;如果值大于 0,则会收缩至不再溢出。按照 flex-shrink 值的比例,值越大的元素收缩得越多。

用 flex-shrink 也能实现上述页面中两列的宽度。首先将两列的 flex-basis 指定为理想的比例(66.67%和 33.33%)。它们的宽度之和加上 1.5em 的间隔就会比容器宽度多出 1.5em。然后将两列的 flex-shrink 设置为 1,这样就会从每列的宽度减掉 0.75em,于是容器就能容纳两列了。代码如代码清单 5-8 所示。

flex 属性常用操作

flex 属性有很多用法。可以像前面的网页那样,用 flex-grow 值或者 flex-basis 百分比定义每列的比例。也可以用于定义固定宽度的列和随着视口缩放的“流动”列。图 5-13 展示了可以用 Flexbox 实现的几种布局。

其中第 3 个例子展示的是“圣杯”布局。众所周知,用 CSS 实现这种布局非常困难。该布局中,两个侧边栏宽度固定,而中间的列是“流动的”,即它会自动填充可用空间。重点是,三列的高度相等,该高度取决于它们的内容。尽管浮动也能实现这种布局,但需要用一些既晦涩又脆弱的技巧。你可以使用不同的弹性子元素,想出很多不同的方式来组合以上的布局

弹性方向

Flexbox 的另一个重要功能是能够切换主副轴方向,用弹性容器的 flex-direction 属性控制。如前面的例子所示,它的初始值(row)控制子元素按从左到右的方向排列;指定 flex-direction: column 能控制弹性子元素沿垂直方向排列(从上到下)。Flexbox 还支持 row-reverse 让元素从右到左排列,column-reverse 让元素从下到上排列(如图 5-14 所示)

在本章的示例网页里,右侧的列将用到 flex-direction 属性让右侧的两个板块按从上到下的顺序排列。这样做似乎多此一举,毕竟右侧的两个板块已经是这样的顺序了。普通的块级元素本来就会这样排列,但是这种布局有一个隐藏的缺陷,给主板块添加更多内容就能看到问题,如图 5-15 所示。

给 column-main 添加一些标题和段落,就会发现主板块超出了右边板块的底部。Flexbox 应该能让两列的高度相等,为什么不起作用了呢?

如图 5-15(虚线区域)所示,其实左右两个弹性子元素是等高的。问题是右边栏内部的两个板块没有扩展到填满右边栏区域。

理想的布局应该如图 5-16 所示。右侧两个板块会扩展高度填满右边栏,尽管左边的内容更长。在 Flexbox 之前,纯 CSS 是无法实现的(需要稍微借助 JavaScript)

上述场景的真正需求是让两列扩展到填满容器的高度。因此要将右边栏(column-sidebar)改为弹性容器,并设置 flex-direction: column。然后给里面的两个板块设置非 0 的 flex-grow 值。按代码清单 5-9 所示,更新你的样式表

以上代码创建了一个嵌套的弹性盒子。对外层的弹性盒子来说,<div class="column-sidebar"> 是弹性子元素,对内部的弹性盒子来说,它就是弹性容器。整体结构如下所示(简洁起见,省略了文字)

<main class="flex">
<div class="column-main tile">
...
</div>
<div class="column-sidebar">
<div class="tile">...</div>
<div class="tile">...</div>
</div>
</div>

内部的弹性盒子的弹性方向为 column,因此主轴发生了旋转,现在变成了从上到下(副轴变成了从左到右)。也就是对于弹性子元素而言,flex-basis、flex-grow 和 flex-shrink现在作用于元素的高度而不是宽度。由于指定了 flex: 1,因此在必要的时候子元素的高度会扩展到填满容器。无论哪边更高,主板块的底部和右边第二个小板块的底部都会对齐。水平弹性盒子的大部分概念同样适用于垂直的弹性盒子(column 或 column-reverse),但是有一点不同:在 CSS 中处理高度的方式与处理宽度的方式在本质上不一样。弹性容器会占据 100%的可用宽度,而高度则由自身的内容来决定。即使改变主轴方向,也不会影响这一本质。

弹性容器的高度由弹性子元素决定,它们会正好填满容器。在垂直的弹性盒子里,子元素的 flex-grow 和 flex-shrink 不会起作用,除非有“外力”强行改变弹性容器的高度。在本章的网页里,“外力” 就是从外层弹性盒子计算出来的高度

理解弹性容器的属性

弹性容器上有好几个属性可以控制弹性子元素的布局。首先是 5.3节介绍过的 flex-direction,下面来看看其他属性

容器属性

弹性子元素属性

使用对齐属性

我们使用以上介绍的一些属性来完成本章的网页。最后一个板块有一个带样式的价格和一个行动召唤(call-to-action,CTA)按钮。完成后的效果如图 5-18 所示。

首先是骨架代码

<div class="tile centered">
<small>Starting at</small>
<div class="cost">
<span class="cost-currency">$</span>
<span class="cost-dollars">20</span>
<span class="cost-cents">.00</span>
</div>
<a class="cta-button" href="/pricing">
Sign up
</a>
</div>

文字 $20.00 包在 <div class="cost"> 中,该元素将作为弹性容器。它有三个弹性子元素,放置三个需要对齐的文字部分($、20、.00)。这里用 span 而不是 div 来放置文字,因为 span 默认就是行内元素。如果因为某些原因 CSS 加载失败,或者浏览器不支持 Flexbox,那么 $20.00 仍然会在一行显示

下面的代码清单里,使用 justify-content 让弹性子元素在弹性容器里水平居中,然后用 align-items 和 align-self 控制文字的垂直对齐。将代码清单 5-11 添加到样式表。

以上代码清单给带样式的 $20.00 设置了 Flexbox 布局,同时定义了 centered 类让剩下的文字居中,还给 CTA 按钮定义了 cta-button 类实现按钮样式。 代码清单里有一个比较特殊的声明:line-height: .7,这是因为每个弹性子元素的文字行高决定了每个子元素的高度,也就是说元素的高度比文字本身的高度多一些。因为 1em 的高度包含了下伸部,而这里的文字刚好没有,所以字符实际上比 1em 要矮。

References

  • 《深入解析 CSS》